package main

import (
	"embed"
	"errors"
	"io/fs"
	"log"
	"net/http"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/zeebo/xxh3"
)

//go:embed all:resources
var assetFs embed.FS

func main() {
	err := run()
	if err != nil {
		log.Fatal(err)
	}
}

type assetInfo struct {
	data            []byte
	dataBr          []byte
	eTag            string
	contentLength   string
	contentLengthBr string

	contentType string
}

func run() error {
	pathToAsset := map[string]*assetInfo{}

	err := fs.WalkDir(assetFs, "resources", func(path string, _ fs.DirEntry, err error) error {
		if err != nil {
			return err
		}

		if !strings.ContainsRune(path, '.') || strings.HasPrefix(path, "/.") {
			// is a directory
			return nil
		}

		data, err := assetFs.ReadFile(path)
		if err != nil {
			return err
		}

		if path == "resources" {
			return nil
		}

		key := strings.TrimPrefix(path, "resources")
		isBr := strings.HasSuffix(key, ".br")
		if isBr {
			key = strings.TrimSuffix(key, ".br")
		}

		info := pathToAsset[key]
		if info == nil {
			info = &assetInfo{}
			pathToAsset[key] = info
		}

		lengthAsString := strconv.Itoa(len(data))
		if isBr {
			info.dataBr = data
			info.contentLengthBr = lengthAsString
		} else {
			info.data = data
			info.contentLength = lengthAsString

			// no mime type on distroless image
			switch {
			case strings.HasSuffix(key, ".woff"):
				info.contentType = "font/woff"
			case strings.HasSuffix(key, ".woff2"):
				info.contentType = "font/woff2"
			case strings.HasSuffix(key, ".svg"):
				info.contentType = "image/svg+xml"
			case strings.HasSuffix(key, ".js"):
				info.contentType = "text/javascript"
			case strings.HasSuffix(key, ".json"):
				info.contentType = "application/json"
			case strings.HasSuffix(key, ".html"):
				info.contentType = "text/html"
			case strings.HasSuffix(key, ".css"):
				info.contentType = "text/css"
			case strings.HasSuffix(key, ".wasm"):
				info.contentType = "application/wasm"
			case strings.HasSuffix(key, ".dictionary"):
				info.contentType = "application/octet-stream"
			case strings.HasSuffix(key, ".ttf"):
				info.contentType = "font/ttf"
			default:
				return errors.New("cannot determinate content-type by file extension: " + filepath.Ext(path))
			}

			// assets are immutable
			if !strings.HasPrefix(key, "/assets/") {
				hash := xxh3.Hash128(data)
				info.eTag = strconv.FormatUint(hash.Hi, 36) + "-" + strconv.FormatUint(hash.Lo, 36)
			}
		}

		return nil
	})
	if err != nil {
		return err
	}

	// no need to keep it anymore
	assetFs = embed.FS{}

	indexHtml := pathToAsset["/index.html"]
	pathToAsset["/"] = indexHtml

	http.Handle("/index.html", http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		newPath := "./"
		q := request.URL.RawQuery
		if q != "" {
			newPath += "?" + q
		}
		writer.Header().Set("Location", newPath)
		writer.WriteHeader(http.StatusMovedPermanently)
	}))
	http.Handle("/", http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		header := writer.Header()

		path := request.URL.Path
		asset := pathToAsset[path]
		if asset == nil {
			if strings.ContainsRune(path, '.') {
				http.NotFound(writer, request)
				return
			}

			// vue router HTML5 mode (https://next.router.vuejs.org/guide/essentials/history-mode.html#html5-mode)
			asset = indexHtml
		}

		header.Set("Vary", "Accept-Encoding")
		header.Set("Content-Type", asset.contentType)

		// https://medium.com/adobetech/an-http-caching-strategy-for-static-assets-configuring-the-server-1192452ce06a
		if asset.eTag == "" {
			header.Set("Cache-Control", "public,max-age=31536000,immutable")
		} else {
			header.Set("Cache-Control", "no-cache")

			if request.Header.Get("If-None-Match") == asset.eTag {
				writer.WriteHeader(http.StatusNotModified)
				return
			}

			header.Set("ETag", asset.eTag)
		}

		if len(asset.dataBr) != 0 && strings.Contains(request.Header.Get("Accept-Encoding"), "br") {
			header.Set("Content-Length", asset.contentLengthBr)
			header.Set("Content-Encoding", "br")
			_, _ = writer.Write(asset.dataBr)
		} else {
			header.Set("Content-Length", asset.contentLength)
			_, _ = writer.Write(asset.data)
		}
	}))
	return http.ListenAndServe(":8080", nil)
}
